package com.hup.utils.commons.hardware;

import lombok.extern.log4j.Log4j2;

import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseEvent;

/**
 * <pre>
 * 实现鼠标Loop功能:在鼠标拖拽到屏幕边缘时,程序控制鼠标越过边缘(实际是让鼠标到达屏幕'横向/纵向'的另一侧);
 * 此过程中,不需要[手工释放按键并将鼠标移到控件上重新拖拽]的操作;
 * 用途如:股票日k界面横向拖拽,调节Timer窗口大小
 * 好处:
 *  在修改/微调跨度很大的数值类属性时
 *  .避免了频繁重新拖拽的操作
 *  .避免了人工重新拖拽时,对原拖拽起点或中途折点产生偏移(使调节的预览效果(如图像)产生不连续的变化效果)
 * </pre>
 * <br>
 * 参考来源:印象中,3dMax、会声会影都有这样的功能,其用于类似 {@link JSpinner}的控件中,
 * 使调节数值类的选项时,可以让鼠标'无限上拽/下拽',
 * 来实现控制数值一直增加/减少的效果;
 * @apiNote 目前仅提供单向loop功能(同一时间仅loop横向或纵向)
 * @author hugan
 * @date 2020/4/21
 */
@Log4j2
public class MouseLooper {

    /**
     * 屏幕边缘距离:鼠标在边缘内时,就开始判断是否loop
     */
    private static final int EDGE = 5;

    private final MlPermitCallback permitCallback;

    /**
     * 鼠标可拖拽的[横向/纵向]范围(占屏幕的比例);
     * 避免鼠标loop前后距离差过大,用户找不到鼠标;
     * 默认 1/3;目前没暴漏设置方法
     */
    private float ratio = 1/3f;
    /**
     * {@link #ratio}
     */
    private int minX, maxX, minY, maxY;

    /**
     * press事件 坐标原点
     */
    private int x0, y0;

    /**
     * 上次的坐标
     */
    private int lastX, lastY;
    /**
     * loop的次数
     */
    private int loopXCount, loopYCount;

    /**
     * @param permitCallback nullable
     */
    public MouseLooper(MlPermitCallback permitCallback) {
        this.permitCallback = permitCallback;
    }

    private boolean canLoop(boolean isLoopY, boolean isAdd) {
        return permitCallback == null || permitCallback.canLoop(isLoopY, isAdd);
    }

    /**
     * 按键按下后,需要激活loop功能时调用
     * 用于记录拖拽原点等信息
     */
    public void mousePressed(MouseEvent e) {
        lastX = x0 = e.getXOnScreen();
        lastY = y0 = e.getYOnScreen();
        loopYCount = 0;
        loopXCount = 0;

        Dimension screen = ScreenUtil.getScreenSize();
        resetDragRange(true, x0, screen.width);
        resetDragRange(false, y0, screen.height);
        log.debug("0=[{},{}], X=[{},{}], Y=[{},{}]", x0, y0, minX, maxX, minY, maxY);
    }

    /**
     * @param isX 横向还是纵向
     * @param pressed0 按下时的坐标
     * @param length 屏幕尺寸
     */
    private void resetDragRange(boolean isX, int pressed0, int length) {
        int min, max;
        float moveRange = length * ratio;
        if (pressed0 < moveRange / 2) {
            //在屏幕[上|左]1/3内
            min = EDGE;
            max = (int) (moveRange - EDGE);
        } else if (pressed0 > length - moveRange / 2) {
            //在屏幕[下|右]1/3内
            max = length - EDGE;
            min = (int) (length - moveRange + EDGE);
        } else {
            //以按下时的位置为中心点,扩充范围
            min = (int) (pressed0 - moveRange / 2 + EDGE);
            max = (int) (pressed0 + moveRange / 2 + EDGE);
        }

        if (isX) {
            minX = min;
            maxX = max;
        } else {
            minY = min;
            maxY = max;
        }
    }

    /**
     * 处理loop功能和计算移动偏移量
     * @return diffX:鼠标距离起始点的diffX
     * @apiNote 调用前,必须在mousePressed事件中调用 {@link #mousePressed(MouseEvent)}
     */
    public int mouseDraggedX(MouseEvent e) {
        int eX = e.getXOnScreen();
        int eY = e.getYOnScreen();
        int diffX;
        I1:
        if (eX <= minX && eX <= lastX) {//到达边缘&&向上/向左移动
            if (!canLoop(false, false)) {
                //不能上/左loop,移动到边缘
                HRobotJni.mouseMove(minX, eY);
                eX = minX;
                break I1;
            }
            //从上/左边缘,移动到下/右:减一圈
            HRobotJni.mouseMove(maxX, eY);
            eX = maxX;
            loopXCount--;

            lastX = maxX + EDGE;//当作是从大(last)变小(e)
            diffX = (eX - x0) + (maxX - minX + 1) * loopXCount + 1;
            return diffX;
        } else if (eX >= maxX && eX >= lastX) {//到达边缘&&向下/向右移动
            if (!canLoop(false, true)) {
                HRobotJni.mouseMove(maxX, eY);
                eX = maxX;
                break I1;
            }
            HRobotJni.mouseMove(minX, eY);
            eX = minX;
            loopXCount++;

            lastX = 0;//当作是从小(last)变大(e)
            diffX = (eX - x0) + (maxX - minX + 1) * loopXCount - 1;
            return diffX;
        }
        //没处在边缘,或者在边缘但不是loop的移动方向
        lastX = eX;
        diffX = (eX - x0) + (maxX - minX + 1) * loopXCount;
        return diffX;
    }

    /**
     * 处理loop功能和计算移动偏移量
     * @return diffY:鼠标距离起始点的diffY
     * @apiNote 调用前,必须在mousePressed事件中调用 {@link #mousePressed(MouseEvent)}
     */
    public int mouseDraggedY(MouseEvent e) {
        int eX = e.getXOnScreen();
        int eY = e.getYOnScreen();
        int diffY;
        I1:
        if (eY <= minY && eY <= lastY) {//到达边缘&&向上/向左移动
            if (!canLoop(true, false)) {
                //不能上/左loop,移动到边缘
                HRobotJni.mouseMove(eX, minY);
                eY = minY;
                break I1;
            }
            //从上/左边缘,移动到下/右:减一圈
            HRobotJni.mouseMove(eX, maxY);
            eY = maxY;
            loopYCount--;

            lastY = maxY + EDGE;//当作是从大(last)变小(e)
            diffY = (eY - y0) + (maxY - minY + 1) * loopYCount + 1;
            return diffY;
        } else if (eY >= maxY && eY >= lastY) {//到达边缘&&向下/向右移动
            if (!canLoop(true, true)) {
                HRobotJni.mouseMove(eX, maxY);
                eY = maxY;
                break I1;
            }
            HRobotJni.mouseMove(eX, minY);
            eY = minY;
            loopYCount++;

            lastY = 0;//当作是从小(last)变大(e)
            diffY = (eY - y0) + (maxY - minY + 1) * loopYCount - 1;
            return diffY;
        }
        //没处在边缘,或者在边缘但不是loop的移动方向
        lastY = eY;
        diffY = (eY - y0) + (maxY - minY + 1) * loopYCount;
        return diffY;
    }

    public interface MlPermitCallback {
        /**
         * @param isLoopY Y方向还是X方向
         * @param isAdd 坐标加还是减
         * @return 能否loop
         */
        boolean canLoop(boolean isLoopY, boolean isAdd);
    }

}
